package dst.ass2.ejb.session;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

import javax.ejb.Remote;
import javax.ejb.Remove;
import javax.ejb.Stateful;
import javax.interceptor.Interceptors;
import javax.persistence.EntityManager;
import javax.persistence.LockModeType;
import javax.persistence.PersistenceContext;
import javax.persistence.TypedQuery;

import dst.ass1.jpa.model.ITaskProcessing;
import dst.ass1.jpa.model.ITaskWorker;
import dst.ass1.jpa.model.IUser;
import dst.ass1.jpa.model.TaskStatus;
import dst.ass1.jpa.model.impl.Metadata;
import dst.ass1.jpa.model.impl.Task;
import dst.ass1.jpa.model.impl.TaskProcessing;
import dst.ass1.jpa.model.impl.User;
import dst.ass2.ejb.dto.AssignmentDTO;
import dst.ass2.ejb.interceptor.AuditInterceptor;
import dst.ass2.ejb.session.exception.AssignmentException;
import dst.ass2.ejb.session.interfaces.ITaskManagementBean;

@Remote(ITaskManagementBean.class)
@Stateful
@Interceptors({AuditInterceptor.class})
public class TaskManagementBean implements ITaskManagementBean {

	@PersistenceContext
	private EntityManager entityManager;

	private boolean loggedIn = false;
	private IUser user;

	private final static Logger LOGGER = Logger.getLogger(TaskManagementBean.class.getName());

	List<AssignmentDTO> cachedAssignments = new ArrayList<AssignmentDTO>(); // Data Transfer Objects cached

	@Override
	public void addTask(Long platformId, Integer numWorkUnits, String context,List<String> settings) throws AssignmentException {

		LOGGER.info("JobmanagementBean - add Task in WorkPlatform: "+ platformId + " and a number of : " + numWorkUnits+ " WorkUnits");

		List<ITaskWorker> availableTaskWorkerOnWorkPlatform = availableTaskWorkerOnWorkPlatform(platformId); //get available TaskWorkers from DB
		
		if (!isCapacityExisting(availableTaskWorkerOnWorkPlatform, numWorkUnits)) {   //if there is not enough space for the Task (requested numWorkUnits)
			LOGGER.info("Not enough Capacity existing for WorkUnits");
			throw new AssignmentException("Not enough Capacity existing for WorkUnits");  //if not exception
		}
		else{		//if there is enough space you can schedule the WorkUnits
			List<Long> ids = scheduleToTaskWorkers(availableTaskWorkerOnWorkPlatform,numWorkUnits); //if there is enough space schedule them
	
			LOGGER.info("numWorkUnits scheduled to Platform");
			
			AssignmentDTO dto = new AssignmentDTO(platformId, numWorkUnits,context, settings, ids); //make a new assignment
			
			this.cachedAssignments.add(dto);  //and add it to the cached list
		}
	}

	/**
	 * gets all TaskWorkers from a specific Platform
	 * and returns all TaskWorkers thats TaskProcessings are neither PROCESSING nor SCHEDULED
	 */
	private List<ITaskWorker> availableTaskWorkerOnWorkPlatform(Long platformId) {

		LOGGER.info("call availableTaskWorkerOnWorkPlatform");

		List<ITaskWorker> taskWorkers = entityManager.createQuery("SELECT t FROM TaskWorker t WHERE t.taskForce.workPlatform.id=:platformId",
				ITaskWorker.class).setParameter("platformId", platformId).getResultList();		//all TaskWorkers of the Platform

		Map<Long, ITaskWorker> taskWorkersOnWorkPlatform = new HashMap<Long, ITaskWorker>();
		for (ITaskWorker tw : taskWorkers) { // iterate over TaskWorkers
			boolean isFree = true;
			for (ITaskProcessing t : tw.getTaskProcessings()) {		//TaskProcessings of TaskWorker
				if (t.getStatus() == TaskStatus.PROCESSING || t.getStatus() == TaskStatus.SCHEDULED) { // if TaskProcessing is Working
					isFree = false; // it is not free
					break; //break because all have to be free
				}
			}

			if (isFree) {  //is all TaskProcessings of TaskWorkers are free
				taskWorkersOnWorkPlatform.put(tw.getId(), tw); // if all are free than add TaskWorker
			}
		}

		
		for (AssignmentDTO a : this.cachedAssignments) { // check if assignments are reserveds
			for (Long id : a.getWorkerIds()) {			
				taskWorkersOnWorkPlatform.remove(id); // remove all that occur already in an assignment in the assignments
			}
		}

		LOGGER.info("There are " + taskWorkersOnWorkPlatform.values().size() +" free TaskWorkers on the WorkPlatform " + platformId);

		return new ArrayList<ITaskWorker>(taskWorkersOnWorkPlatform.values());
	}
	
	
	/**
	 * Simply proofs if there are enough available Working units on the specific platform
	 */
	private boolean isCapacityExisting(List<ITaskWorker> taskWorkersOnWorkPlatform,Integer numWorkUnits) {
		LOGGER.info("call isCapacityExisting");

		int availableWorkUnits = 0;
		for (ITaskWorker t : taskWorkersOnWorkPlatform) {
			availableWorkUnits += t.getWorkUnitCapacity();  //sum up alle WorkUnitCapacities from the available Worker on WorkPlatform
		}
		
		boolean isCapacityLeft=(numWorkUnits < availableWorkUnits);
		
		LOGGER.info("isCapacityExisting returned "+isCapacityLeft);
		
		return isCapacityLeft;  //are there enough available WorkUnitCapacities
	}

	/**
	 * choose TaskWorkers from a List until all workUnits are depleted
	 */
	private List<Long> scheduleToTaskWorkers (List<ITaskWorker> availableTaskWorkerOnWorkPlatform, Integer numWorkUnits) {

		LOGGER.info("call scheduleToTaskWorkers");

		int unassignedUnits = numWorkUnits; // substract till 0 so that everything is assigned
		List<Long> ids = new LinkedList<Long>();

		for (ITaskWorker tw : availableTaskWorkerOnWorkPlatform) { // go through all Workers and add as long the workUnits are remaining
			ids.add(tw.getId());						 //add TaskWorker
			unassignedUnits -= tw.getWorkUnitCapacity(); // substract unassigned and actual Work Units from TaskWorker

			if (unassignedUnits <= 0) { // if all is assigned
				LOGGER.info(ids.size() + " TaskWorkers are assigned in method scheduleToTaskWorkers");
				break;
			}
		}

		return ids;
	}

	/**
	 * login the user with the username and password
	 */
	@Override
	public void login(String username, String password) throws AssignmentException {

		LOGGER.info("TaskManagementBean login with " + username);

		if(username==null || username.isEmpty() || password==null || password.isEmpty())
			throw new AssignmentException("No User with name: " + username);
		
		User foundUser = entityManager.createQuery("SELECT u FROM User u WHERE username=:name", User.class) // get ONE user with username because its unique
				.setParameter("name", username).getSingleResult();

		if (foundUser == null) { // if not found then exception
			LOGGER.info("No user found with Name: " + username);
			throw new AssignmentException("No User with name: " + username);
		}
		else {
			boolean passwordsAreEqual=false;
			
			try {
				passwordsAreEqual = isPasswordsEqual(password.getBytes(),foundUser.getPassword());
			} catch (NoSuchAlgorithmException e) {
				e.printStackTrace();
			}
	
			if (!passwordsAreEqual) { // if the two Hashes are equal
				LOGGER.info("Not a valid Username and Password Combination: " + username);
				throw new AssignmentException( "Not a valid Username and Password Combination: " + username);
			} else {
				this.loggedIn = true; // setting the loggedIn indicator
				this.user = foundUser; // setting the user that successfully logged in
				LOGGER.info("User: " + username + " is successfully logged in");
			}
		}

	}

	
	private boolean isPasswordsEqual (byte[] enteredPassword, byte[] hashedPassword) throws NoSuchAlgorithmException{
		MessageDigest md = null;
		
		try {
			md = MessageDigest.getInstance("MD5"); // get MD5 hashing instance
		} catch (NoSuchAlgorithmException e) {
			LOGGER.info("Error retrieving MD5 MessageDigest");
			throw e;
		}

		byte[] passwdEntered = md.digest(enteredPassword); // get Hash from the Password entered
		
		return Arrays.equals(passwdEntered, hashedPassword);
	}
	
	/**
	 * remove the Assignments that are assigned to a specific platform
	 */
	@Override
	public void removeTasksForPlatform(Long platformId) {
		LOGGER.info("called removeTasksForPlatform");

		ArrayList<AssignmentDTO> remove = new ArrayList<AssignmentDTO>();

		for (AssignmentDTO assignment : this.cachedAssignments) { // go over all Assignments
			if (assignment.getPlatformId().longValue() == platformId .longValue()) { // if Platform matches
				remove.add(assignment); // add assignment to remove list
				LOGGER.info("Removing the Assignment " + assignment+ " for WorkPlatform " + platformId);
			}
		}
		
		this.cachedAssignments.removeAll(remove); // remove all Assignments that matched the platform
		LOGGER.info("Removed all assignments for the Platform " + platformId);
	}

	
	/**
	 * Submit the assignments
	 */
	@Override
    @Remove(retainIfException = true)
    public void submitAssignments() throws AssignmentException {
		LOGGER.info("called submitAssignments");
		
        if (!this.loggedIn) {		//if user is logged in
        	LOGGER.info("Log in befor calling the method submitAssignments");
            throw new AssignmentException("Not logged in");
        }
        else{
	        //getting all TaskWorkers
	        TypedQuery<ITaskWorker> query = entityManager.createQuery("SELECT tw FROM TaskWorker tw", ITaskWorker.class);
	        List<ITaskWorker> taskWorkers = query.getResultList();
	
	        lockTaskWorkers(taskWorkers);  //locking the TaskWorkers
	
	        for (AssignmentDTO dto : this.cachedAssignments) {		//check if all Assignments are Ok to Submit
	            if (!isValidAssignment(taskWorkers, dto)) {						//is it ok to persists -> proof TaskProcessing once more
	                throw new AssignmentException("Assignment is invalid");  
	            }
	        }
	
	        for (AssignmentDTO dto : this.cachedAssignments) {
	            persistAssignment(taskWorkers, dto);			//persist every Assignment Object
	            LOGGER.info("Persists AssignmentDTO: "+dto);
	        }	
	
	        
	        LOGGER.info("Flush persisted data in TaskManagementBean");
	        entityManager.flush();  //flush it after persist
        }
    }

	/**
	 * Get the TaskWorker out of the List with a id
	 */
	private ITaskWorker getTaskWorkerWithId(List<ITaskWorker> taskWorkers,Long id){
		for(ITaskWorker tw : taskWorkers){
			if(tw.getId() == id)
				return tw;
		}
		
		return null;
	}
	
	/**
	 * Persist all the Assignments
	 */
    private void persistAssignment(List<ITaskWorker> taskWorkers, AssignmentDTO dto) {
        final Date now = new Date();

        IUser user = entityManager.createQuery("SELECT u FROM User u WHERE username = :name", IUser.class)
        		.setParameter("name", this.user.getUsername())
        		.getSingleResult();  //retrieve user from DB

        Metadata metadata = new Metadata();
        Task task = new Task();
        TaskProcessing tp = new TaskProcessing();
        List<ITaskWorker> assignmentTaskWorkers = new ArrayList<ITaskWorker>();

        for (long id : dto.getWorkerIds()) {				//set for all TaskWorkers the TaskProcessing
            final ITaskWorker tw = getTaskWorkerWithId(taskWorkers, id);

            tw.addTaskProcessing(tp);
            assignmentTaskWorkers.add(tw);		//add TaskWorker to list
        }

        //------------Setting all the information------------
        
        //in Metadata
        metadata.setSettings(dto.getSettings());			
        metadata.setContext(dto.getContext());
        
     	//in User
        user.addTask(task);  

        //in TaskProcessing
        tp.setTaskWorkers(assignmentTaskWorkers);
        tp.setStart(now);
        tp.setTask(task);
        tp.setStatus(TaskStatus.SCHEDULED);
        
        //in Task
        task.setMetadata(metadata);
        task.setTaskProcessing(tp);
        task.setUser(user);
        task.setAssignedWorkUnits(dto.getNumWorkUnits());
        task.setPaid(false);

        //persit it
        entityManager.persist(task);
    }

    
    /**
     * Locking the Entity before submitting
     */
    private void lockTaskWorkers(List<ITaskWorker> taskWorkers) {
        for (ITaskWorker tw: taskWorkers) {
            entityManager.lock(tw, LockModeType.PESSIMISTIC_READ);
        }
    }

    /**
     * checks if specific TaskProcessing of TaskWorkers that are equal with Assignment TaskWorkers
     * are not running
     */
    private boolean isValidAssignment(List<ITaskWorker> taskWorkers, AssignmentDTO dto) {
        for (long id : dto.getWorkerIds()){
            ITaskWorker tw = getTaskWorkerWithId(taskWorkers,id);  //get TaskWorker
            entityManager.refresh(tw);				//refresh to be sure

            for (ITaskProcessing tp : tw.getTaskProcessings()) {   //all TaskProcessing have to be free
                entityManager.refresh(tp);  //refresh to be sure

                if (tp.getStatus() == TaskStatus.PROCESSING || tp.getStatus() == TaskStatus.SCHEDULED) {  //if running
                    return false;
                }
            }
        }
        
        return true;
    }

	@Override
	public List<AssignmentDTO> getCache() {
		return this.cachedAssignments;
	}

}
